/*
 * Copyright (c) 2011, the Dart project authors.
 * 
 * Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.dart.tools.ui.internal.text.editor;

import com.google.dart.tools.ui.DartToolsPlugin;

import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.IExecutionListener;
import org.eclipse.core.commands.NotHandledException;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.custom.VerifyKeyListener;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.commands.ICommandService;

/**
 * Exit strategy for commands that want to fold repeated execution into one compound edit. See
 * {@link org.eclipse.jface.text.IRewriteTarget#endCompoundChange()
 * IRewriteTarget.endCompoundChange}. As long as a strategy is installed on an {@link ITextViewer},
 * it will detect the end of a compound operation when any of the following conditions becomes true:
 * <ul>
 * <li>the viewer's text widget loses the keyboard focus</li>
 * <li>the mouse is clicked or double clicked inside the viewer's widget</li>
 * <li>a command other than the ones specified is executed</li>
 * <li>the viewer receives any key events that are not modifier combinations</li>
 * </ul>
 * <p>
 * If the end of a compound edit is detected, any registered {@link ICompoundEditListener}s are
 * notified and the strategy is disarmed (spring-loaded).
 * </p>
 */
public final class CompoundEditExitStrategy {
  /**
   * Listens for events that may trigger the end of a compound edit.
   */
  private final class EventListener implements MouseListener, FocusListener, VerifyKeyListener,
      IExecutionListener {

    @Override
    public void focusGained(FocusEvent e) {
    }

    /*
     * @see org.eclipse.swt.events.FocusListener#focusLost(org.eclipse.swt.events .FocusEvent)
     */
    @Override
    public void focusLost(FocusEvent e) {
      // losing focus ends the change
      fireEndCompoundEdit();
    }

    /*
     * @see org.eclipse.swt.events.MouseListener#mouseDoubleClick(org.eclipse.swt
     * .events.MouseEvent)
     */
    @Override
    public void mouseDoubleClick(MouseEvent e) {
      // mouse actions end the compound change
      fireEndCompoundEdit();
    }

    /*
     * @see org.eclipse.swt.events.MouseListener#mouseDown(org.eclipse.swt.events .MouseEvent)
     */
    @Override
    public void mouseDown(MouseEvent e) {
      // mouse actions end the compound change
      fireEndCompoundEdit();
    }

    @Override
    public void mouseUp(MouseEvent e) {
    }

    @Override
    public void notHandled(String commandId, NotHandledException exception) {
    }

    @Override
    public void postExecuteFailure(String commandId, ExecutionException exception) {
    }

    @Override
    public void postExecuteSuccess(String commandId, Object returnValue) {
    }

    /*
     * @see org.eclipse.core.commands.IExecutionListener#preExecute(java.lang.String,
     * org.eclipse.core.commands.ExecutionEvent)
     */
    @Override
    public void preExecute(String commandId, ExecutionEvent event) {
      // any command other than the known ones end the compound change
      for (int i = 0; i < fCommandIds.length; i++) {
        if (commandId.equals(fCommandIds[i])) {
          return;
        }
      }
      fireEndCompoundEdit();
    }

    /*
     * @see org.eclipse.swt.custom.VerifyKeyListener#verifyKey(org.eclipse.swt.events .VerifyEvent)
     */
    @Override
    public void verifyKey(VerifyEvent event) {
      // any key press that is not a modifier combo ends the compound change
      final int maskWithoutShift = SWT.MODIFIER_MASK & ~SWT.SHIFT;
      if ((event.keyCode & SWT.MODIFIER_MASK) == 0 && (event.stateMask & maskWithoutShift) == 0) {
        fireEndCompoundEdit();
      }
    }

  }

  private final String[] fCommandIds;
  private final EventListener fEventListener = new EventListener();
  private final ListenerList fListenerList = new ListenerList(ListenerList.IDENTITY);

  private ITextViewer fViewer;
  private StyledText fWidgetEventSource;

  /**
   * Creates a new strategy, equivalent to calling {@linkplain #CompoundEditExitStrategy(String[])
   * CompoundEditExitStrategy(new String[] &#x7b; commandId &#x7d;)}.
   * 
   * @param commandId the command id of the repeatable command
   */
  public CompoundEditExitStrategy(String commandId) {
    if (commandId == null) {
      throw new NullPointerException("commandId"); //$NON-NLS-1$
    }
    fCommandIds = new String[] {commandId};
  }

  /**
   * Creates a new strategy, ending upon execution of any command other than the ones specified.
   * 
   * @param commandIds the ids of the repeatable commands
   */
  public CompoundEditExitStrategy(String[] commandIds) {
    for (int i = 0; i < commandIds.length; i++) {
      if (commandIds[i] == null) {
        throw new NullPointerException("commandIds[" + i + "]"); //$NON-NLS-1$ //$NON-NLS-2$
      }
    }
    fCommandIds = new String[commandIds.length];
    System.arraycopy(commandIds, 0, fCommandIds, 0, commandIds.length);
  }

  /**
   * Adds a compound edit listener. Multiple registration is possible. Note that the receiver is
   * automatically disarmed before the listeners are notified.
   * 
   * @param listener the new listener
   */
  public void addCompoundListener(ICompoundEditListener listener) {
    fListenerList.add(listener);
  }

  /**
   * Installs the receiver on <code>viewer</code> and arms it. After this call returns, any
   * registered listeners will be notified if a compound edit ends.
   * 
   * @param viewer the viewer to install on
   */
  public void arm(ITextViewer viewer) {
    disarm();
    if (viewer == null) {
      throw new NullPointerException("editor"); //$NON-NLS-1$
    }
    fViewer = viewer;
    addListeners(fViewer);
  }

  /**
   * Disarms the receiver. After this call returns, any registered listeners will be not be notified
   * any more until <code>install</code> is called again. Note that the listeners are not removed.
   * <p>
   * Note that the receiver is automatically disarmed when the end of a compound edit has been
   * detected and before the listeners are notified.
   * </p>
   */
  public void disarm() {
    if (isInstalled()) {
      removeListeners(fViewer);
      fViewer = null;
    }
  }

  /**
   * Removes a compound edit listener. If <code>listener</code> is registered multiple times, an
   * arbitrary instance is removed. If <code>listener</code> is not currently registered, nothing
   * happens.
   * 
   * @param listener the listener to be removed.
   */
  public void removeCompoundListener(ICompoundEditListener listener) {
    fListenerList.remove(listener);
  }

  private void addListeners(ITextViewer viewer) {
    fWidgetEventSource = viewer.getTextWidget();
    if (fWidgetEventSource != null) {
      fWidgetEventSource.addVerifyKeyListener(fEventListener);
      fWidgetEventSource.addMouseListener(fEventListener);
      fWidgetEventSource.addFocusListener(fEventListener);
    }

    ICommandService commandService = (ICommandService) PlatformUI.getWorkbench().getAdapter(
        ICommandService.class);
    if (commandService != null) {
      commandService.addExecutionListener(fEventListener);
    }
  }

  private void fireEndCompoundEdit() {
    disarm();
    Object[] listeners = fListenerList.getListeners();
    for (int i = 0; i < listeners.length; i++) {
      ICompoundEditListener listener = (ICompoundEditListener) listeners[i];
      try {
        listener.endCompoundEdit();
      } catch (Exception e) {
        DartToolsPlugin.log(e);
      }
    }
  }

  private boolean isInstalled() {
    return fViewer != null;
  }

  private void removeListeners(ITextViewer editor) {
    ICommandService commandService = (ICommandService) PlatformUI.getWorkbench().getAdapter(
        ICommandService.class);
    if (commandService != null) {
      commandService.removeExecutionListener(fEventListener);
    }

    if (fWidgetEventSource != null) {
      fWidgetEventSource.removeFocusListener(fEventListener);
      fWidgetEventSource.removeMouseListener(fEventListener);
      fWidgetEventSource.removeVerifyKeyListener(fEventListener);
      fWidgetEventSource = null;
    }
  }

}
